Skip to content

🤖 refactor: cut the taskService/workspaceService dependency cycle at a typed seam - #3996

Merged
ibetitsmike merged 20 commits into
mainfrom
mike/task-workspace-seam
Aug 29, 2026
Merged

🤖 refactor: cut the taskService/workspaceService dependency cycle at a typed seam#3996
ibetitsmike merged 20 commits into
mainfrom
mike/task-workspace-seam

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Cuts the TaskService/WorkspaceService concrete-class dependency cycle with one-directional typed ports, then pays for the new contract by deleting the code and test scaffolding the ports make obsolete. The full branch is a net simplification: +676/−1010 (net −334) across 11 files.

Background

TaskService and WorkspaceService imported each other's concrete classes, so neither side had a stated contract: every public member of each class was implicitly reachable from the other, and tests smuggled partial fakes through as unknown as casts. An architecture change must shrink the codebase, so the seam had to delete more than it declares.

Implementation

  • taskWorkspaceSeam.ts is the single home for the two ports and every type they share:
    • WorkspaceHost (~38 methods): what TaskService may call; WorkspaceService implements it.
    • AgentTaskIntegration (12 methods): what WorkspaceService may call; TaskService implements it, wired via setAgentTaskIntegration (the old setTaskService is deleted).
    • ArchiveWorkspaceOptions, SendMessageInternalOptions (formerly a 56-line inline literal), WorkspaceLiveActivity, AgentTaskStatus, and the archive untracked-path helpers moved here instead of being duplicated; StreamErrorRecoveryOutcome is imported from its canonical agentSession export. The AgentTaskStatus re-export shim in taskService is gone; importers use the seam.
  • Host call placement is byte-position-identical to main. The call sites are race-hardened sink rechecks and admission-gate interleavings, so placement is behavior; this PR only changes what the callers are allowed to know about each other.
  • Invariants moved into types: method-level optional chaining on both ports (integration?.method?.()) and the "test harnesses mock a narrow surface" hedges are deleted because the interfaces now guarantee the methods exist.

Test shrink (where the net-negative lives)

  • Typed fakes replace all as unknown as TaskService/WorkspaceService casts: makeAgentTaskIntegrationFake() (shared testUtils) and a satisfies WorkspaceHost central factory.
  • Deleted tests the port makes unwritable or that only asserted wiring: a phantom cleanupReportedDescendantsAfterArchive guard (the method never existed in production), an archive lock pass-through assertion, and four private updateAgentStatus non-invocation spies whose positive behavior tests remain.
  • Near-identical siblings collapsed into test.each tables (send/resume interrupted-task lifecycle, winding-down gate, auto-resume reset, foreground-wait backgrounding); every case is preserved.
  • createWorkspaceServiceMocks lost its dead stubs (waitForIdle, deleteWorktree, updateAgentStatus never existed on the port), its hand-written 37-line return annotation, and unconsumed return entries; 23 copy-pasted createWorkspace mock blocks collapsed into two shared helpers typed via Parameters<WorkspaceHost["create"]>; redundant conditional-spread forwarding in two harnesses became direct pass-through.

Validation

  • make static-check green; typecheck is the enforcement mechanism for the seam (it caught two false "unused symbol" audit claims during development).
  • Full taskService/workspaceService/heartbeatService suites: 1022 tests, failures limited to the 4 pre-existing flakes reproduced identically on untouched trunk in a clean worktree.
  • Structural greps: zero remaining cross-references between the two services; the seam imports neither.

Risks

The production diff is receiver-renames, type moves, and deleted dead plumbing; runtime control flow is unchanged (one .bind indirection replaced by a direct guarded call). Regression risk concentrates in the test consolidation: a test.each translation error could weaken a race guard. Mitigated by preserving each original case row and assertion set, and by the suites passing with an unchanged test count on the task side.


Generated with xum • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: a6bf1b2a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike
ibetitsmike force-pushed the mike/task-workspace-seam branch from a6bf1b2 to 3f94aa7 Compare August 28, 2026 08:27
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 3f94aa704c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Branch reworked per reviewer directive: the seam now pays for itself. Net diff vs main is +676/−1010 (−334). New commits dedupe seam types into taskWorkspaceSeam.ts, encode port invariants in types instead of optional chaining, and delete seam-obsoleted test scaffolding (phantom-method guard, wiring-only tests, private-spy tests, copy-pasted mock blocks, dead stubs).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 122fb5bf0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

_Generated with `xum` • Model: `openai:gpt-5.6-sol` • Thinking: `high`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high -->
Review follow-up: the four per-command handlers are only reachable through
processSlashCommand now, and /plan open lost its dedicated tests in the
result-based rewrite.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P1: getDraft captured at command invocation reports that render's
input, so async commands cleared newer drafts on consume and never fired
restore-if-empty.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2: text equality cannot distinguish a retyped identical draft from
the original invocation. Commands already clear through their own
clear-input actions (matching trunk), so the terminal clear was additive
and could only destroy mid-phase drafts.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2 follow-up: with the terminal consume-path clear gone, /dream and
/refine left the executed command re-runnable in the composer. Emit
clear-input from the handlers so commands own their composer effects.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
The seam file is now the single documented home for ArchiveWorkspaceOptions,
SendMessageInternalOptions, and WorkspaceLiveActivity instead of duplicating
workspaceService declarations. StreamErrorRecoveryOutcome comes from its
canonical agentSession export. The AgentTaskStatus re-export shim is gone;
importers use the seam. Method-level optional chaining on both typed ports is
removed: the interfaces guarantee the methods, so the narrow-mock hedges and
their comments no longer apply.
… suite

The typed AgentTaskIntegration port makes several tests unwritable or
redundant: the phantom cleanupReportedDescendantsAfterArchive guard (method
never existed in production), the archive lock pass-through wiring assertion,
and four private updateAgentStatus non-invocation spies whose positive
registerSession behavior tests remain. Near-identical send/resume lifecycle,
winding-down, auto-resume, and foreground-wait-backgrounding siblings collapse
into table-driven tests preserving every case, and dead fake stubs the code
under test never reads are dropped.
…d host port

createWorkspaceServiceMocks loses its dead stubs (waitForIdle, deleteWorktree,
updateAgentStatus never existed on WorkspaceHost), its hand-written 37-line
return annotation, and return entries nothing consumes; the
removeQueuedMessagesByDedupeKeyPrefix override is wired instead of the one
call site mutating the built host. 23 copy-pasted createWorkspace mock blocks
collapse into two shared helpers, and the redundant conditional-spread
forwarding in both harnesses becomes direct pass-through since the factory
already defaults absent overrides.
Drops fake overrides that restate makeAgentTaskIntegrationFake defaults,
types makeWorkspaceTurnCreateMock's rest args via Parameters<WorkspaceHost['create']>
instead of an unknown[] cast, and calls withTaskTreeLifecycleLock directly
rather than through a bound closure.
@ibetitsmike
ibetitsmike force-pushed the mike/task-workspace-seam branch from 122fb5b to e05dc05 Compare August 28, 2026 19:22
@ibetitsmike
ibetitsmike changed the base branch from main to mike/deep-tool-modules August 28, 2026 19:22
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was unmerged and was not in the queue Aug 29, 2026
Base automatically changed from mike/deep-tool-modules to main August 29, 2026 05:45
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit 3dfccfa Aug 29, 2026
53 of 74 checks passed
@ibetitsmike
ibetitsmike deleted the mike/task-workspace-seam branch August 29, 2026 05:58
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Aug 31, 2026
…oder#4012)

## Summary

Splits the 36-method `WorkspaceHost` grab-bag on the task/workspace seam
into five role interfaces named for what task-side callers do
(`WorkspaceTurnHost`, `TurnAdmissionHost`, `WorkspaceLifecycleHost`,
`WorkspaceProvisioningHost`, `WorkspaceMetadataHost`), keeps
`WorkspaceHost` as their intersection so no call site or wiring changes,
and collapses the ~180-line hand-rolled test mock onto one shared
`makeWorkspaceHostFake`.

## Background

coder#3996 cut the taskService/workspaceService dependency cycle at a typed
seam, but the seam stayed shallow: one interface mirroring 36 of
WorkspaceService's internal mechanics. Every task test stubbed all 36
methods through a ~180-line mock in `taskService.test.ts` (reached from
318 call sites), and the seam, service, and test harness churned in
lockstep on every change. This builds on coder#3996 rather than reverting it:
same seam, deeper interface. Refactor coder#5 from the 2026-08-29
architecture review (evidence at main @ f04e0f8).

## Implementation

- `taskWorkspaceSeam.ts`: the five role interfaces group methods by
caller intent (turn execution, queue/admission probes, archive/remove
lifecycle, child-workspace provisioning, metadata/events). Every method
signature is byte-identical to before; `WorkspaceHost` is now
`WorkspaceTurnHost & TurnAdmissionHost & WorkspaceLifecycleHost &
WorkspaceProvisioningHost & WorkspaceMetadataHost`. `TaskService` (the
only production consumer) legitimately uses all five roles, so its
single constructor param stays; new narrow consumers can now depend on
one role instead of the full host.
- `taskWorkspaceSeam.testUtils.ts`: adds framework-free
`makeWorkspaceHostFake(overrides)` beside the existing
`makeAgentTaskIntegrationFake`, carrying the harness's default stub
semantics (granted archive hold, "keep"-style snapshot eligibility,
sanitizer no-op).
- `taskService.test.ts`: `createWorkspaceServiceMocks` shrinks from ~180
lines to ~55 on top of the shared fake, with a mapped type over `keyof
WorkspaceHost` replacing the hand-written 36-entry overrides list.
Returned mock handles and the archive/remove locked-sink aliasing are
preserved, so all 318 harness call sites are untouched.

## Net LOC delta vs main (f04e0f8)

- Production (`taskWorkspaceSeam.ts`): **+18** (+103/-85)
- Tests (`taskService.test.ts` + `taskWorkspaceSeam.testUtils.ts`):
**-76** (+112/-188)
- Overall: **-58**

Irreducible production additions: the five role interface declarations
plus the intersection type (the point of the refactor), and the archive
race-invariant docs on `ArchiveWorkspaceOptions`, which review feedback
correctly required keeping verbatim rather than counting as savings.
Test additions are `makeWorkspaceHostFake`'s default bodies (moved from
the harness, now reusable by any seam consumer's tests).

## Validation

- Remote dogfood UAT ran against the pushed SHA and passed: sub-agent
spawn/report/interrupt/reawaken, workspace turns (new + queued follow-up
race), archive/unarchive including `interrupt_active` and live-activity
refusal, heartbeats, tree listing, and monitor wakes. The UAT runner
additionally verified the emitted JavaScript is byte-identical between
base and feature for the production file.
- Whole-file `bun test` of `taskService.test.ts`,
`workspaceService.test.ts`, `heartbeatService.test.ts`,
`tools/task_list.test.ts`: the fail set is identical to a clean worktree
at base f04e0f8 (3 pre-existing host-environment failures; none
branch-attributable).

## Risks

Low. The production change is type-only interface restructuring with
byte-identical emitted JS; regression surface is the test-harness
consolidation, which preserves each mock's default behavior and aliasing
semantics.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh` • Cost: `$29.31`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh
costs=29.31 -->

## Stack

Layer 4/10 of the architecture refactor stack (net -5,101 LOC overall).
This PR's diff is only this layer, against
`mike/arch-peer-message-broker`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant